<?php
/*
	Copyright 2006-2007 Alex Djachenko http://www.my-site.ru
	Authors: Alex Djachenko
    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/

class sharpobj_storage_mysql extends sharpobj_storage_proto_sql
{
	private $connect=null;
	private $connect_read=null;
	private $format=null;

    /**
     * Сделать sql-запрос к базе данных (функция низкого уровня)
     * $type - тип ожидаемого результата запроса
     *    sqlres - не обработанный результат
     *    assocvalue - одна строка, переведенная в ассоциативный массив
     *    rowvalue - одна строка, переведенная в нумерованный массив
     *    onevalue - первая запись первой строки результата
     *    action - возвращает true, если запрос выполнен без ошибок
     *    insert - возвращает id последней вставленной записи
     * $sql - sql-запрос
     *
     * @param string $sql
     * @param string $type
     */
    function sqlquery($sql,$type,$readonly=false)
	{
		$res = null;
		$sql = $this->queryReplace($sql);
		$connect = &$this->connect;
		if ($readonly AND
		        $this->connect_read)
		{
		    // Соединение  с сервером только для чтения отсутствует
            $connect = &$this->connect_read;
		}
		// Выполняем запрос
		$time_start = microtime(true);
		$sqlres = mysql_query($sql,$connect);
		$time_stop = microtime(true);
		// Проверяем результат
		if (!$sqlres)
		{
			$type = 'error';
			mst_error('Ошибка в sql-запросе',"Ошибка (".mysql_error().") в запросе ({$sql})",2);
		}
		// Сохраняем отладочную информацию
		if (defined('SHARPOBJ_SQLLOG'))
		{
		    global $sharpobj_log_fulltime_exec;
		    global $sharpobj_log_path;
		    if (!$sharpobj_log_path)
		    {
		        mst_modmkdatadir('sharpobj','dat/log/',true,true);
		        $log_mkadds = explode(" ", microtime());
		        $log_referer = @$_SERVER["HTTP_REFERER"];
		        $sharpobj_log_path = mst_modpath('sharpobj','dat/log/'
		                    .date('ymdHis',$_SERVER["REQUEST_TIME"])
		                    .'.'.round($log_mkadds[0]*10)
		                    .'_'.$_SERVER["REMOTE_ADDR"]
		                    .'_'.$_SERVER["REMOTE_PORT"].'.log',true,true);
                // Заголовок файла
                error_log("head\t{$_SERVER["REQUEST_URI"]}\t{$log_referer}\n",3,$sharpobj_log_path);
		    }
		    // Вычисляем время исполнения всех запросов в текущей сессии
		    $sharpobj_log_fulltime_exec += ($time_stop - $time_start); 
		    // Экранируем спецсимволы в sql-запросе
		    $log_sql = addcslashes($sql,"\n\r\t");
		    // $log_trace=debug_backtrace();
		    // Записывае информацию в файл
			error_log("sql\t".round($time_stop - $time_start,4)."\t".round($sharpobj_log_fulltime_exec,4)."\t".mst_gettimefromstart(4)
			        ."\t".get_class($this)
			        ."\t{$type}\t{$log_sql}\n",3,$sharpobj_log_path);
		}
		switch ($type)
		{
			case 'sqlres':
				$res = $sqlres;
			break;
			case 'assocvalue':
				$res = mysql_fetch_assoc($sqlres);
				mysql_free_result($sqlres);
			break;
			case 'rowvalue':
				$res = mysql_fetch_row($sqlres);
				mysql_free_result($sqlres);
			break;
			case 'onevalue':
				if (mysql_num_rows($sqlres)<1)
				{
					// Не возвращено ни одной строки
					$res = false;
				}else
				{
					//
					$res = mysql_result($sqlres,0,0);
				}
				mysql_free_result($sqlres);
			break;
			case 'action':
				// mysql_free_result($sqlres);
				$res = true;
			break;
			case 'insert':
				// $res = mysql_insert_id();
				$res = $this->id2obj(mysql_insert_id());
				// mysql_free_result($sqlres);
			break;
			case 'error':
				// mysql_free_result($sqlres);
				$res = false;
			break;
			default:
				mst_error('Неправильный тип запроса','Тип запроса '.$type.' не поддерживается',2);
				$res = false;
			break;
		}
		return $res;
	}

	// *******************************
	// Базовый набор функций работы с записями
	// *******************************
    /**
     * Получить запись по ее id
     *
     * @param int $id
     * @return bool
     */
    function get($id)
	{
	    $idobj = $this->id2obj($id);
	    return $this->sqlqueryGet(" WHERE `{$this->idfield}`='{$idobj->get('sql')}'");
	}
    /**
     * Вставить запись в базу данных
     *
     * @param sharpobj_data_interface_item $data
     */
    function insert(sharpobj_data_item_data $data)
	{
		// Формируем SQL-запрос
		$sql = 'INSERT INTO `'.$this->tbname.'`'
		.' ('
		.$this->format->getColNames($data, '`', ', ','`'.$this->tbname.'`.',true)
		.')'
		.' VALUES ('
		.$this->format->getColVals($data,'sql',"'", ', ',true)
		.');';
		// echo $sql;
		return $this->sqlquery($sql,'insert');
	}
    /**
     * Обновить запись с $id
     * Если $emptyreplace == true - пустые записи тоже обновляются
     * Если $id == null - id береться из контейнера
     *
     * @param int $id
     * @param sharpobj_data_interface_item $data
     * @param bool $emptyreplace
     * @param sharpobj_data_interface_item $data
     */
    function update(sharpobj_data_item_data $data,$id=null,$emptyreplace=false)
	{
		// Создаем контейнер для id
		$idobj = $this->id2obj($id);
		
		if ($idobj->get('plain')>0)
		{
			// ID задан принудительно
			// $idobj->set('plain',$id);
		}elseif ($data instanceof sharpobj_data_item_data  AND $data->getId()>0)
		{
			// ID задан в объекте
			$idobj = new sharpobj_data_item_int();
			$idobj->set('plain',$data->getId());
		}else
		{
			mst_error('Не передан id',"Для операции UPDATE необходимо задать id записи "
			."принудительно или передать вместе с контейнером",1);
			return false;
		}
		// Формируем SQL-запрос
		$sql = 'UPDATE `'.$this->tbname.'`'
		.' SET '
		.$this->format->getColNameVals($data,'sql',
					"`", '`'.$this->tbname.'`.',"'",'=',', ',
					!$emptyreplace)
		.' WHERE `'.$this->idfield.'`='
		."'".$idobj->get('sql')."'"
		.' LIMIT 1';

		return $this->sqlquery($sql,'action');
	}
    /**
     * Удалить запись по ее id
     *
     * @param int $id
     * @return bool
     */
    function delete($id)
	{

		$idobj = $this->id2obj($id);
		// Формируем SQL-запрос
		$sql = 'DELETE FROM `'.$this->tbname.'`'
		.' WHERE `'.$this->idfield.'`='
		."'".$idobj->get('sql')."'"
		.' LIMIT 1';
		// echo $sql;
		return $this->sqlquery($sql,'action');
	}
	
	function count()
	{
		return intval($this->sqlquery("SELECT count(*) FROM #!table#",'onevalue'));
	}
	
	/**
	 *  Получить список всех записей в БД	
	 * @return void
	 */
	function getListAll($from=null,$limit=null)
	{
		//
		return $this->sqlqueryList("SELECT #!id# FROM #!table# "
			.$this->sqlgenLimit($from,$limit));
	}
	// *******************************
	// Дополнительные функции работы с записями
	// *******************************
	function countUp($id,$field)
	{
		//
		$field = addslashes($field);
		// Создаем контейнер для id
		$idobj = $this->id2obj($id);
		if ($idobj->get('plain')<=0)
		{
			mst_error('Не передан id',
					"Для операции countUp необходимо задать id записи ",1);
			return false;
		}
		return $this->sqlquery("UPDATE #!table# "
						." SET `{$field}`=(`{$field}`+1) "
						."WHERE #!id#='{$idobj->get('sql')}'",'action');		
	}
	function countDown($id,$field)
	{
		//
		$field = addslashes($field);
		// Создаем контейнер для id
		$idobj = $this->id2obj($id);
		if ($idobj->get('plain')<=0)
		{
			mst_error('Не передан id',
					"Для операции countUp необходимо задать id записи ",1);
			return false;
		}
		return $this->sqlquery("UPDATE #!table# "
						." SET `{$field}`=(`{$field}`-1) "
						."WHERE #!id#='{$idobj->get('sql')}'",'action');	
	}
	/**
	 *  Получить список всех записей в БД	
	 * @return void
	 */
	function getListFilter($filter,$from=null,$limit=null,$order_by='')
	{
		//
        $filter_sql = '';
        // Создаем фильтр
        foreach ($filter as $key=>$val)
        {
            // Получаем клон элемента нужного формата
            $item = $this->format->getObj($key);
            $item->clear();
            // Устанавливаем данные
            $item->set('plain',$val);
            $field = addslashes($this->format->getColName($key));
            // Добавляем условие
            if (mb_strlen($filter_sql)>0){$filter_sql .= ' AND';}
            if (is_null($item->getP()))
            {
                $filter_sql .= " `{$field}` IS NULL ";
            }else
            {
                $filter_sql .= " `{$field}`='{$item->get('sql')}' ";
            }
            
        }
	    if (mb_strlen($filter_sql)>0){$filter_sql = ' WHERE '.$filter_sql;}
	    if (mb_strlen($order_by)>0){$order_by = ' ORDER BY '.$order_by;}
		return $this->sqlqueryList("SELECT #!id# FROM #!table# ".$filter_sql
			.$this->sqlgenLimit($from,$limit).$order_by.';');
	}
	// *******************************
	// Дополнительные функции, для работы с sql-БД
	// *******************************
    /**
     * Выбрать один объект из базы по sql-условию
     * @param string $sql
     */	
    function sqlqueryGet($sqlwhere)
	{
		// $idobj = new sharpobj_data_item_int();
		// $idobj->set('plain',$id);
		// $idobj = $this->id2obj($id);
		// Формируем SQL-запрос
		$sql = 'SELECT '
		."`{$this->idfield}`,".$this->format->getColNames(null,  '`', ', ','`'.$this->tbname.'`.')
		.' FROM `'.$this->tbname.'`'
		.' '.$sqlwhere
		// .' WHERE `'.$this->idfield.'`='
		// ."'".$idobj->get('sql')."'"
		.' LIMIT 1;';
		// echo $sql;
		$mysqlres = $this->sqlquery($sql,'rowvalue',true);
		// Проверяем наличие результата
		if (false===$mysqlres)
		{	// Результат отсутствует
			return false;
		}
		// Получаем список колонок и полей
		$fields = $this->format->getColsArray(null, false);
		// Клонируем пустой объект с данными
		$dataobj = $this->format->getObj();
        // Устанавливаем id записи
        $thisres = each($mysqlres);
		$dataobj->setId($thisres['value']);
		// Заполняем объект данными
		foreach ($fields as $field_datapath=>$field_colname)
		{
			// Получаем соответствующую колонку из результата запроса
			$thisres = each($mysqlres);
			// Помещаем данные в объект
			$dataobj->set('sql',$thisres['value'],$field_datapath);
			
		}
		return $dataobj;
	}
    /**
     * Выбрать список объектов из базы по sql-запросу
     * В первой колонке должны идти ID записей в данном хранилище
     * Остальные поля не важны
     * @param string $sql
     */	
    function sqlqueryList($sql)
	{
		// Создаем контейнер для результата
		$result = new sharpobj_data_item_array();
		// Выполняем запрос к БД
		if (!($sqlres = $this->sqlquery($sql,'sqlres',true)))
		{
			return $result;
		}
		while ($res = mysql_fetch_row($sqlres))
		{
			//
			$result->add(null,$this->get($res[0]));
		}
		// Очищаем результат запроса
		mysql_free_result($sqlres);
		return $result;
	}
    /**
     * Выбрать одно значение из БД
     * @param string $sql
     */	
    function sqlqueryOneValue($sql)
	{
		return $this->sqlquery($sql,'onevalue',true);
	}
    /**
     * Выбрать одно integer-значение из БД
     * @param string $sql
     */
    function sqlqueryOneValueInt($sql)
	{
		$res = $this->sqlquery($sql,'onevalue',true);
		if (!sharpobj_iserror($res) AND $res !==false)
		{
		    $res = intval($res);
		}
		return $res;
	}
    /**
     * Выбрать нумерованный массив из базы
     * @param string $sql
     */
    function sqlqueryRowValue($sql)
	{
		return $this->sqlquery($sql,'rowvalue',true);
	}
    /**
     * Выбрать ассоциативный массив из базы
     * @param string $sql
     */	
    function sqlqueryAssocValue($sql)
	{
		return $this->sqlquery($sql,'assocvalue',true);
	}
    /**
     * Выполнить действие в базе данных
     * @param string $sql
     */	
    function sqlqueryAction($sql)
	{
		return $this->sqlquery($sql,'action');
	}
	// *******************************
	// Вспомогательные функции
	// *******************************
	function getObj($datapath=null)
	{
		return $this->format->getObj($datapath);
	}
    function setConnection(&$connect,$readonly=false)
	{
		if ($readonly)
	    {
		    $this->connect_read = &$connect;
	    }else
	    {
	        $this->connect = &$connect;
	    }
		
	}
    function &getConnection($readonly=false)
	{
	    if ($readonly)
	    {
		    return $this->connect_read;
	    }else
	    {
	        return $this->connect;
	    }
	}
	 /**
	 * Возвращаем дамп для создания базы данных (без вторичных индексов!)
	 * @return string (sql)
	 */
	function sqlDBCreate($replace=null)
	{
		// Входные параметры
		if (is_null($replace)){$replace = array();}
		
		// Начало sql-запроса
		$sql = "CREATE TABLE {$this->tbname()}\n(\n";
		// ID
		$sql .= "{$this->idfield()} int(11) NOT NULL auto_increment";
		// Объявление индекса
		
		// Получаем формат колонок
		foreach ($this->format->getColsTypesArray() as $name=>$param)
		{
			// Заголовок колонки
			$sql .=",\n`{$name}` ";
			if (isset($replace[($param['datapath'])]))
			{
				// У нас есть опции для этого поля вместо стандартных
				$sql .= $replace[($param['datapath'])];
				continue;
			}
			switch ($param['type'])
			{
				case 'int':
					// Задаем опции по умолчанию
					$len_max = 11;
					$nullval = "default NULL";
					if (is_array($param['options']))
					{
						// Обрабатываем опции
						// Максимальная длина
						if (isset($param['options']['len_max']))
						{
							$len_max = $param['options']['len_max'];
						}
						// NULL
						if (isset($param['options']['null']) 
								AND $param['options']['null']===false)
						{
							$nullval = "NOT NULL";
						}
					}
					$sql .= "int({$len_max}) {$nullval}";
				break;
				case 'double':
				case 'float':
					// Задаем опции по умолчанию
					$nullval = "default NULL";
					if (is_array($param['options']))
					{
						// Обрабатываем опции
						// NULL
						if (isset($param['options']['null']) 
								AND $param['options']['null']===false)
						{
							$nullval = "NOT NULL";
						}
					}
					$sql .= "double {$nullval}";
				break;
				case 'string':
				case 'email':
				case 'html':
				case 'path':
				case 'phone':
				case 'row':
				case 'smallatword':
				case 'text':
				case 'url':
					// Задаем опции по умолчанию
					$len_max = 255;
					$nullval = "default NULL";
					if (is_array($param['options']))
					{
						// Обрабатываем опции
						// Максимальная длина
						if (isset($param['options']['len_max']))
						{
							$len_max = $param['options']['len_max'];
						}
						
						if (isset($param['options']['null']) 
								AND $param['options']['null']===false)
						{
							$nullval = "NOT NULL";
						}
					}
					if ($len_max>255)
					{
						$sql .= "TEXT {$nullval}";
					}else
					{
						$sql .= "varchar({$len_max}) collate utf8_unicode_ci {$nullval}";
					}
				break;
				case '':
				default:
					// Ошибка - тип не задан или неизвестен, возвращаем что-нибудь
					// Потом сделаем возвращение ошибки
					$sql = "varchar(255) collate utf8_unicode_ci default NULL";
					mst_error('Ошибка генерации схемы',
					"Тип данных {$param['type']} отсутствует и не может быть добавлен в таблицу",2,true);
				break;
			}
			if (mb_strlen($param['comment'])>0)
			{
				$sql .= " COMMENT '".addslashes($param['comment'])."'";
			}
		}
		
		$sql .= ",\nPRIMARY KEY ({$this->idfield()})";
		// Создаем индексы
		foreach ($this->format->getIndexesArray() as $index)
		{
			// Массив колонок
			$colnames = '';
			$unique = '';
			// Собираем колонки индекса
			if (is_array($index['colnames']))
			{
				foreach ($index['colnames'] as $colname)
				{
					if (strlen($colnames)>0)
					{
						$colnames .= ',';
					}
					$colnames .= "`{$colname}`";
				}
			}else
			{
				// Одна колонка
				$colnames = "`{$index['colnames']}`";
			}
			// Уникальность индекса
			if ($index['type'] === 'unique')
			{
				$unique = 'UNIQUE ';
			}
			// Добавляем SQL-код для создания индекса
			$sql .= ",\n{$unique}KEY `{$index['name']}` ($colnames)";
		}
		// Завершение sql-запроса
		$sql .= "\n)\nENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci";
		$sql .= "\nCOMMENT='".addslashes($this->format->getComment())."';";
		return $sql;
	}
	/**
	 * Создать таблицу в БД
	 */
	function createTable()
	{
		return $this->sqlquery($this->sqlDBCreate(),'action');
	}
	/**
	 * Удалить таблицу из БД
	 */
	function dropTable()
	{
		return $this->sqlquery("DROP TABLE #!table#;",'action');
	}
	// *******************************
	// Служебные функции для внутреннего пользования
	// *******************************
	function __construct(sharpobj_storage_format $format, $opt)
	{
		$this->format = $format;
		$this->tbname = $opt['tbname'];
		$this->idfield = $opt['idfield'];
		// Добавляем соединение по умолчанию (потом можно переопределить)
		$this->setConnection(sharpobj_dbconn_get('mysql'));
		$this->setConnection(sharpobj_dbconn_get('mysql','readonly',true),true);
	}	
    

}
// Объявляем класс под старым именем для совместимости
class sharpobj_storage_simplemysql extends sharpobj_storage_mysql
{

}
?>